// Copyright 2019 Google LLC.
// Use of this source code is governed by a BSD-style license that can be found in the LICENSE file.

#include "modules/skplaintexteditor/include/editor.h"

#include "include/core/SkCanvas.h"
#include "include/core/SkExecutor.h"
#include "include/core/SkPath.h"
#include "src/base/SkUTF.h"

#include "modules/skplaintexteditor/src/shape.h"

#include <algorithm>
#include <cfloat>

using namespace SkPlainTextEditor;

static inline SkRect offset(SkRect r, SkIPoint p)
{
    return r.makeOffset((float)p.x(), (float)p.y());
}

static constexpr SkRect kUnsetRect{ -FLT_MAX, -FLT_MAX, -FLT_MAX, -FLT_MAX };

static bool valid_utf8(const char *ptr, size_t size)
{
    return SkUTF::CountUTF8(ptr, size) >= 0;
}

// Kind of like Python's readlines(), but without any allocation.
// Calls f() on each line.
// F is [](const char*, size_t) -> void
template <typename F> static void readlines(const void *data, size_t size, F f)
{
    const char *start = (const char *)data;
    const char *end = start + size;
    const char *ptr = start;
    while (ptr < end) {
        while (*ptr++ != '\n' && ptr < end) {
        }
        size_t len = ptr - start;
        SkASSERT(len > 0);
        f(start, len);
        start = ptr;
    }
}

static StringSlice remove_newline(const char *str, size_t len)
{
    return SkASSERT((str != nullptr) || (len == 0)),
        StringSlice(str, (len > 0 && str[len - 1] == '\n') ? len - 1 : len);
}

void Editor::markDirty(TextLine *line)
{
    line->fBlob = nullptr;
    line->fShaped = false;
    line->fWordBoundaries = std::vector<bool>();
}

void Editor::setFont(SkFont font)
{
    if (font != fFont) {
        fFont = std::move(font);
        fNeedsReshape = true;
        for (auto &l : fLines) {
            this->markDirty(&l);
        }
    }
}

void Editor::setFontMgr(sk_sp<SkFontMgr> fontMgr)
{
    fFontMgr = fontMgr;
    fNeedsReshape = true;
    for (auto &l : fLines) {
        this->markDirty(&l);
    }
}

void Editor::setWidth(int w)
{
    if (fWidth != w) {
        fWidth = w;
        fNeedsReshape = true;
        for (auto &l : fLines) {
            this->markDirty(&l);
        }
    }
}
static SkPoint to_point(SkIPoint p)
{
    return { (float)p.x(), (float)p.y() };
}

Editor::TextPosition Editor::getPosition(SkIPoint xy)
{
    Editor::TextPosition approximatePosition;
    this->reshapeAll();
    for (size_t j = 0; j < fLines.size(); ++j) {
        const TextLine &line = fLines[j];
        SkIRect lineRect = { 0, line.fOrigin.y(), fWidth, j + 1 < fLines.size() ? fLines[j + 1].fOrigin.y() : INT_MAX };
        if (const SkTextBlob *b = line.fBlob.get()) {
            SkIRect r = b->bounds().roundOut();
            r.offset(line.fOrigin);
            lineRect.join(r);
        }
        if (!lineRect.contains(xy.x(), xy.y())) {
            continue;
        }
        SkPoint pt = to_point(xy - line.fOrigin);
        const std::vector<SkRect> &pos = line.fCursorPos;
        for (size_t i = 0; i < pos.size(); ++i) {
            if (pos[i] != kUnsetRect && pos[i].contains(pt.x(), pt.y())) {
                return Editor::TextPosition{ i, j };
            }
        }
        approximatePosition = { xy.x() <= line.fOrigin.x() ? 0 : line.fText.size(), j };
    }
    return approximatePosition;
}

static inline bool is_utf8_continuation(char v)
{
    return ((unsigned char)v & 0b11000000) == 0b10000000;
}

static const char *next_utf8(const char *p, const char *end)
{
    if (p < end) {
        do {
            ++p;
        } while (p < end && is_utf8_continuation(*p));
    }
    return p;
}

static const char *align_utf8(const char *p, const char *begin)
{
    while (p > begin && is_utf8_continuation(*p)) {
        --p;
    }
    return p;
}

static const char *prev_utf8(const char *p, const char *begin)
{
    return p > begin ? align_utf8(p - 1, begin) : begin;
}

SkRect Editor::getLocation(Editor::TextPosition cursor)
{
    this->reshapeAll();
    cursor = this->move(Editor::Movement::kNowhere, cursor);
    if (fLines.size() > 0) {
        const TextLine &cLine = fLines[cursor.fParagraphIndex];
        SkRect pos = { 0, 0, 0, 0 };
        if (cursor.fTextByteIndex < cLine.fCursorPos.size()) {
            pos = cLine.fCursorPos[cursor.fTextByteIndex];
        }
        pos.fRight = pos.fLeft + 1;
        pos.fLeft -= 1;
        return offset(pos, cLine.fOrigin);
    }
    return SkRect{ 0, 0, 0, 0 };
}

static size_t count_char(const StringSlice &string, char value)
{
    size_t count = 0;
    for (char c : string) {
        if (c == value) {
            ++count;
        }
    }
    return count;
}

Editor::TextPosition Editor::insert(TextPosition pos, const char *utf8Text, size_t byteLen)
{
    if (!valid_utf8(utf8Text, byteLen) || 0 == byteLen) {
        return pos;
    }
    pos = this->move(Editor::Movement::kNowhere, pos);
    fNeedsReshape = true;
    if (pos.fParagraphIndex < fLines.size()) {
        fLines[pos.fParagraphIndex].fText.insert(pos.fTextByteIndex, utf8Text, byteLen);
        this->markDirty(&fLines[pos.fParagraphIndex]);
    } else {
        SkASSERT(pos.fParagraphIndex == fLines.size());
        SkASSERT(pos.fTextByteIndex == 0);
        fLines.push_back(Editor::TextLine(StringSlice(utf8Text, byteLen)));
    }
    pos = Editor::TextPosition{ pos.fTextByteIndex + byteLen, pos.fParagraphIndex };
    size_t newlinecount = count_char(fLines[pos.fParagraphIndex].fText, '\n');
    if (newlinecount > 0) {
        StringSlice src = std::move(fLines[pos.fParagraphIndex].fText);
        std::vector<TextLine>::const_iterator next = fLines.begin() + pos.fParagraphIndex + 1;
        fLines.insert(next, newlinecount, TextLine());
        TextLine *line = &fLines[pos.fParagraphIndex];
        readlines(src.begin(), src.size(),
            [&line](const char *str, size_t l) { (line++)->fText = remove_newline(str, l); });
    }
    return pos;
}

Editor::TextPosition Editor::remove(TextPosition pos1, TextPosition pos2)
{
    pos1 = this->move(Editor::Movement::kNowhere, pos1);
    pos2 = this->move(Editor::Movement::kNowhere, pos2);
    auto cmp = [](const Editor::TextPosition &u, const Editor::TextPosition &v) { return u < v; };
    Editor::TextPosition start = std::min(pos1, pos2, cmp);
    Editor::TextPosition end = std::max(pos1, pos2, cmp);
    if (start == end || start.fParagraphIndex == fLines.size()) {
        return start;
    }
    fNeedsReshape = true;
    if (start.fParagraphIndex == end.fParagraphIndex) {
        SkASSERT(end.fTextByteIndex > start.fTextByteIndex);
        fLines[start.fParagraphIndex].fText.remove(start.fTextByteIndex, end.fTextByteIndex - start.fTextByteIndex);
        this->markDirty(&fLines[start.fParagraphIndex]);
    } else {
        SkASSERT(end.fParagraphIndex < fLines.size());
        auto &line = fLines[start.fParagraphIndex];
        line.fText.remove(start.fTextByteIndex, line.fText.size() - start.fTextByteIndex);
        line.fText.insert(start.fTextByteIndex, fLines[end.fParagraphIndex].fText.begin() + end.fTextByteIndex,
            fLines[end.fParagraphIndex].fText.size() - end.fTextByteIndex);
        this->markDirty(&line);
        fLines.erase(fLines.begin() + start.fParagraphIndex + 1, fLines.begin() + end.fParagraphIndex + 1);
    }
    return start;
}

static void append(char **dst, size_t *count, const char *src, size_t n)
{
    if (*dst) {
        ::memcpy(*dst, src, n);
        *dst += n;
    }
    *count += n;
}

size_t Editor::copy(TextPosition pos1, TextPosition pos2, char *dst) const
{
    size_t size = 0;
    pos1 = this->move(Editor::Movement::kNowhere, pos1);
    pos2 = this->move(Editor::Movement::kNowhere, pos2);
    auto cmp = [](const Editor::TextPosition &u, const Editor::TextPosition &v) { return u < v; };
    Editor::TextPosition start = std::min(pos1, pos2, cmp);
    Editor::TextPosition end = std::max(pos1, pos2, cmp);
    if (start == end || start.fParagraphIndex == fLines.size()) {
        return size;
    }
    if (start.fParagraphIndex == end.fParagraphIndex) {
        SkASSERT(end.fTextByteIndex > start.fTextByteIndex);
        auto &str = fLines[start.fParagraphIndex].fText;
        append(&dst, &size, str.begin() + start.fTextByteIndex, end.fTextByteIndex - start.fTextByteIndex);
        return size;
    }
    SkASSERT(end.fParagraphIndex < fLines.size());
    const std::vector<TextLine>::const_iterator firstP = fLines.begin() + start.fParagraphIndex;
    const std::vector<TextLine>::const_iterator lastP = fLines.begin() + end.fParagraphIndex;
    const auto &first = firstP->fText;
    const auto &last = lastP->fText;

    append(&dst, &size, first.begin() + start.fTextByteIndex, first.size() - start.fTextByteIndex);
    for (auto line = firstP + 1; line < lastP; ++line) {
        append(&dst, &size, "\n", 1);
        append(&dst, &size, line->fText.begin(), line->fText.size());
    }
    append(&dst, &size, "\n", 1);
    append(&dst, &size, last.begin(), end.fTextByteIndex);
    return size;
}

static inline const char *begin(const StringSlice &s)
{
    return s.begin();
}

static inline const char *end(const StringSlice &s)
{
    return s.end();
}

static size_t align_column(const StringSlice &str, size_t p)
{
    if (p >= str.size()) {
        return str.size();
    }
    return align_utf8(begin(str) + p, begin(str)) - begin(str);
}

// returns smallest i such that list[i] > value.  value > list[i-1]
// Use a binary search since list is monotonic
template <typename T> static size_t find_first_larger(const std::vector<T> &list, T value)
{
    return (size_t)(std::upper_bound(list.begin(), list.end(), value) - list.begin());
}

static size_t find_closest_x(const std::vector<SkRect> &bounds, float x, size_t b, size_t e)
{
    if (b >= e) {
        return b;
    }
    SkASSERT(e <= bounds.size());
    size_t best_index = b;
    float best_diff = ::fabsf(bounds[best_index].x() - x);
    for (size_t i = b + 1; i < e; ++i) {
        float d = ::fabsf(bounds[i].x() - x);
        if (d < best_diff) {
            best_diff = d;
            best_index = i;
        }
    }
    return best_index;
}

Editor::TextPosition Editor::move(Editor::Movement move, Editor::TextPosition pos) const
{
    if (fLines.empty()) {
        return { 0, 0 };
    }
    // First thing: fix possible bad input values.
    if (pos.fParagraphIndex >= fLines.size()) {
        pos.fParagraphIndex = fLines.size() - 1;
        pos.fTextByteIndex = fLines[pos.fParagraphIndex].fText.size();
    } else {
        pos.fTextByteIndex = align_column(fLines[pos.fParagraphIndex].fText, pos.fTextByteIndex);
    }

    SkASSERT(pos.fParagraphIndex < fLines.size());
    SkASSERT(pos.fTextByteIndex <= fLines[pos.fParagraphIndex].fText.size());

    SkASSERT(pos.fTextByteIndex == fLines[pos.fParagraphIndex].fText.size() ||
        !is_utf8_continuation(fLines[pos.fParagraphIndex].fText.begin()[pos.fTextByteIndex]));

    switch (move) {
        case Editor::Movement::kNowhere:
            break;
        case Editor::Movement::kLeft:
            if (0 == pos.fTextByteIndex) {
                if (pos.fParagraphIndex > 0) {
                    --pos.fParagraphIndex;
                    pos.fTextByteIndex = fLines[pos.fParagraphIndex].fText.size();
                }
            } else {
                const auto &str = fLines[pos.fParagraphIndex].fText;
                pos.fTextByteIndex = prev_utf8(begin(str) + pos.fTextByteIndex, begin(str)) - begin(str);
            }
            break;
        case Editor::Movement::kRight:
            if (fLines[pos.fParagraphIndex].fText.size() == pos.fTextByteIndex) {
                if (pos.fParagraphIndex + 1 < fLines.size()) {
                    ++pos.fParagraphIndex;
                    pos.fTextByteIndex = 0;
                }
            } else {
                const auto &str = fLines[pos.fParagraphIndex].fText;
                pos.fTextByteIndex = next_utf8(begin(str) + pos.fTextByteIndex, end(str)) - begin(str);
            }
            break;
        case Editor::Movement::kHome: {
            const std::vector<size_t> &list = fLines[pos.fParagraphIndex].fLineEndOffsets;
            size_t f = find_first_larger(list, pos.fTextByteIndex);
            pos.fTextByteIndex = f > 0 ? list[f - 1] : 0;
        } break;
        case Editor::Movement::kEnd: {
            const std::vector<size_t> &list = fLines[pos.fParagraphIndex].fLineEndOffsets;
            size_t f = find_first_larger(list, pos.fTextByteIndex);
            if (f < list.size()) {
                pos.fTextByteIndex = list[f] > 0 ? list[f] - 1 : 0;
            } else {
                pos.fTextByteIndex = fLines[pos.fParagraphIndex].fText.size();
            }
        } break;
        case Editor::Movement::kUp: {
            SkASSERT(pos.fTextByteIndex < fLines[pos.fParagraphIndex].fCursorPos.size());
            float x = fLines[pos.fParagraphIndex].fCursorPos[pos.fTextByteIndex].left();
            const std::vector<size_t> &list = fLines[pos.fParagraphIndex].fLineEndOffsets;
            size_t f = find_first_larger(list, pos.fTextByteIndex);
            // list[f] > value.  value > list[f-1]
            if (f > 0) {
                // not the first line in paragraph.
                pos.fTextByteIndex =
                    find_closest_x(fLines[pos.fParagraphIndex].fCursorPos, x, (f == 1) ? 0 : list[f - 2], list[f - 1]);
            } else if (pos.fParagraphIndex > 0) {
                --pos.fParagraphIndex;
                const auto &newLine = fLines[pos.fParagraphIndex];
                size_t r = newLine.fLineEndOffsets.size();
                if (r > 0) {
                    pos.fTextByteIndex = find_closest_x(newLine.fCursorPos, x, newLine.fLineEndOffsets[r - 1],
                        newLine.fCursorPos.size());
                } else {
                    pos.fTextByteIndex = find_closest_x(newLine.fCursorPos, x, 0, newLine.fCursorPos.size());
                }
            }
            pos.fTextByteIndex = align_column(fLines[pos.fParagraphIndex].fText, pos.fTextByteIndex);
        } break;
        case Editor::Movement::kDown: {
            const std::vector<size_t> &list = fLines[pos.fParagraphIndex].fLineEndOffsets;
            float x = fLines[pos.fParagraphIndex].fCursorPos[pos.fTextByteIndex].left();

            size_t f = find_first_larger(list, pos.fTextByteIndex);
            if (f < list.size()) {
                const auto &bounds = fLines[pos.fParagraphIndex].fCursorPos;
                pos.fTextByteIndex =
                    find_closest_x(bounds, x, list[f], f + 1 < list.size() ? list[f + 1] : bounds.size());
            } else if (pos.fParagraphIndex + 1 < fLines.size()) {
                ++pos.fParagraphIndex;
                const auto &bounds = fLines[pos.fParagraphIndex].fCursorPos;
                const std::vector<size_t> &l2 = fLines[pos.fParagraphIndex].fLineEndOffsets;
                pos.fTextByteIndex = find_closest_x(bounds, x, 0, l2.size() > 0 ? l2[0] : bounds.size());
            } else {
                pos.fTextByteIndex = fLines[pos.fParagraphIndex].fText.size();
            }
            pos.fTextByteIndex = align_column(fLines[pos.fParagraphIndex].fText, pos.fTextByteIndex);
        } break;
        case Editor::Movement::kWordLeft: {
            if (pos.fTextByteIndex == 0) {
                pos = this->move(Editor::Movement::kLeft, pos);
                break;
            }
            const std::vector<bool> &words = fLines[pos.fParagraphIndex].fWordBoundaries;
            SkASSERT(words.size() == fLines[pos.fParagraphIndex].fText.size());
            do {
                --pos.fTextByteIndex;
            } while (pos.fTextByteIndex > 0 && !words[pos.fTextByteIndex]);
        } break;
        case Editor::Movement::kWordRight: {
            const StringSlice &text = fLines[pos.fParagraphIndex].fText;
            if (pos.fTextByteIndex == text.size()) {
                pos = this->move(Editor::Movement::kRight, pos);
                break;
            }
            const std::vector<bool> &words = fLines[pos.fParagraphIndex].fWordBoundaries;
            SkASSERT(words.size() == text.size());
            do {
                ++pos.fTextByteIndex;
            } while (pos.fTextByteIndex < text.size() && !words[pos.fTextByteIndex]);
        } break;
    }
    return pos;
}

void Editor::paint(SkCanvas *c, PaintOpts options)
{
    this->reshapeAll();
    if (!c) {
        return;
    }

    c->drawPaint(SkPaint(options.fBackgroundColor));

    SkPaint selection = SkPaint(options.fSelectionColor);
    auto cmp = [](const Editor::TextPosition &u, const Editor::TextPosition &v) { return u < v; };
    for (TextPosition pos = std::min(options.fSelectionBegin, options.fSelectionEnd, cmp),
                      end = std::max(options.fSelectionBegin, options.fSelectionEnd, cmp);
        pos < end; pos = this->move(Editor::Movement::kRight, pos)) {
        SkASSERT(pos.fParagraphIndex < fLines.size());
        const TextLine &l = fLines[pos.fParagraphIndex];
        c->drawRect(offset(l.fCursorPos[pos.fTextByteIndex], l.fOrigin), selection);
    }

    if (fLines.size() > 0) {
        c->drawRect(Editor::getLocation(options.fCursor), SkPaint(options.fCursorColor));
    }

    SkPaint foreground = SkPaint(options.fForegroundColor);
    for (const TextLine &line : fLines) {
        if (line.fBlob) {
            c->drawTextBlob(line.fBlob.get(), line.fOrigin.x(), line.fOrigin.y(), foreground);
        }
    }
}

void Editor::reshapeAll()
{
    if (fNeedsReshape) {
        if (fLines.empty()) {
            fLines.push_back(TextLine());
        }
        float shape_width = (float)(fWidth);
#ifdef SK_EDITOR_GO_FAST
        SkSemaphore semaphore;
        std::unique_ptr<SkExecutor> executor = SkExecutor::MakeFIFOThreadPool(100);
        int jobCount = 0;
        for (TextLine &line : fLines) {
            if (!line.fShaped) {
                executor->add([&]() {
                    ShapeResult result = Shape(line.fText.begin(), line.fText.size(), fFont, fLocale, shape_width);
                    line.fBlob = std::move(result.blob);
                    line.fLineEndOffsets = std::move(result.lineBreakOffsets);
                    line.fCursorPos = std::move(result.glyphBounds);
                    line.fWordBoundaries = std::move(result.wordBreaks);
                    line.fHeight = result.verticalAdvance;
                    line.fShaped = true;
                    semaphore.signal();
                }
                ++jobCount;
            });
        }
        while (jobCount-- > 0) {
            semaphore.wait();
        }
#else
        for (TextLine &line : fLines) {
            if (!line.fShaped) {
                ShapeResult result =
                    Shape(line.fText.begin(), line.fText.size(), fFont, fFontMgr, fLocale, shape_width);
                line.fBlob = std::move(result.blob);
                line.fLineEndOffsets = std::move(result.lineBreakOffsets);
                line.fCursorPos = std::move(result.glyphBounds);
                line.fWordBoundaries = std::move(result.wordBreaks);
                line.fHeight = result.verticalAdvance;
                line.fShaped = true;
            }
        }
#endif
        int y = 0;
        for (TextLine &line : fLines) {
            line.fOrigin = { 0, y };
            y += line.fHeight;
        }
        fHeight = y;
        fNeedsReshape = false;
    }
}
